var GownFullState = Base.extend({
	constructor: function()
	{
		this.interpreter = new GFFunctionalInterpreter;
		Debug.present && (this.interpreter.msgout_fun = function(str) { Debug.trace(0,'GFFunctionalInterpreterException: %s',str); });
		this.postfix_code = GownFullConfig.default_output_code;
		this.preedit = '';
		this.display_preedit = '';
		this.keyboard_observers = new GFList;
		this.keyboard_pending_rm = new Array();
	},
	SaveSelection: function()
	{
		var adapter = GownFullObjectAdapter.GetAdapter(this.last_focus_obj);

		if(adapter) this.stored_selection = adapter.GetSelection();
	},
	RestoreSelection: function()
	{
		var adapter = GownFullObjectAdapter.GetAdapter(this.last_focus_obj);

		if(adapter && this.stored_selection) {
			adapter.RestoreSelection(this.stored_selection);
			this.stored_selection = null;
		}	
	},
	
	interpreter: null,
	postfix_code: null,
	preedit: null,
	display_preedit: null,
	sel_preedit_adapter: null,

	last_focus_obj: null,
	last_sent_string: null,

	keyboard_observers: null,
	keyboard_pending_rm: null,
	keyboard_observers_inloop: false,

	available: true,
	disable_modifycode: false,

	im_current: 0,

	stored_selection: null,

	modifycode_minibuf: null,
});

var GownFull = Base.extend({
	constructor: function(builder)
	{
		var i;
		var x = this;

		// Create Module Manager
		this.modman = new GownFullModuleManager(this);
		// Create State stack
		this.state_stack = new Array();
		// Create current state
		this.state = new GownFullState;
		this.im_array = new Array();
		this.im_array[0] = { displayname: $GFS('Disabled'), modname: null, type: GownFull.DISABLED };
		this.LoadGFF();

		this.builder = builder;

		if(GownFull.instance) {
			Debug.trace(0,'GownFull: Warning! more than one instance of GownFull created.');
		}
		GownFull.instance = this;

		builder.BuildCSS();
		this.mainwin = builder.BuildMainWindow(this);
		this.candwin = builder.BuildCandidateListWindow();
		this.browserwin = builder.BuildBrowserWindow();
		this.miniwin_man = builder.BuildMiniBufferManager(this);
		this.tooltipwin = builder.BuildTooltipWindow();
		this.menu = builder.BuildMenu(this);

		builder.SetHandler(function(obj,type,key,m) { return x.keyboard_handler(obj,type,key,m); },
				function(obj) { return x.onfocus_handler(obj); },
				function(obj) { return x.lostsel_handler(obj); });


		// Create Input Method event handler
		this._internal_keyboard_observer = function(obj,type,key,modifiers,b)
		{
			// IM should not react if a observer has already "prevent(+ed)default"
			if(!b) return false;

			// check if the character is :
			if(!x.state.disable_modifycode && type == Const.KEY_TYPE_PRESS && key == 58) {
				x.StartModifyOutputCode(obj);
				return false;
			}
			var im;

			// checkout current IM
			im = x.GetCurrentInputMethod();
			if(!im) return true;

			switch(type) {
			case Const.KEY_TYPE_UP: return im.keyup_handler(obj,key,modifiers);
			case Const.KEY_TYPE_PRESS: return im.keypress_handler(obj,key,modifiers);
			case Const.KEY_TYPE_DOWN: return im.keydown_handler(obj,key,modifiers);
			default: return true; // should not happen
			}
		};
		this.AddKeyboardObserver(this._internal_keyboard_observer);

		builder.PostCreateGownFull();
	},

	modman: null,
	state_stack: null,
	state: null,

	mainwin: null,
	candwin: null,
	browserwin: null,
	tooltipwin: null,
	miniwin_man: null,

	menu: null,
	builder: null,

	// Input Method array
	im_array: null,
	// { displayname: null, modname: null, code, type }

	LoadGFF: function()
	{
		var _this = this;
/*		GFFunctionalInterpreter.RegisterFunctional('dump',function(x) {
			var i;
			var y = x;
		
			Debug.trace(0,'Dump Functional:');
			Debug.trace(0,'x=%s',x);
			i = 0;
			while(x instanceof Function) {
				Debug.trace(0,'%d: %s',i,x());
				x = x(true);
				i++;
			}
			return y;
		});
*/
		// data exchange object
		// { target, value, htmlp }
		GFFunctionalInterpreter.RegisterFunctional('output',function(x) {
			if(x instanceof Function) {
				var data = x();

				if(!data) return x;
				// check if data looks like a "string"
				if(data instanceof Object) {
					if(data.htmlp) _this._SendHTML(data.target,data.value);
					else if(data.value) _this._SendString(data.target,data.value);
					else {
						Debug.trace(0,'Functional (output): Unknown data exchange object.');
					}
				}
				else _this._SendString(null,String(data));
				return x;
			}
			// let it undefined
	});
		
	},
	UpdateUI: function()
	{
		var im;

		im = this.GetCurrentInputMethod();
		if(!im) this.mainwin.UpdateIMName($GFS('Disabled'));
		else this.mainwin.UpdateIMName(im.IMName());

		var obj = this.state.last_focus_obj;
		if(obj) {
			var adapter;

			adapter = GownFullObjectAdapter.GetAdapter(obj);
			if(adapter) adapter.SetFocus();
			else if(obj.focus) obj.focus();
		}
		this.ResetPreedit();
	},
	PushState: function()
	{
		var new_state;

		// before we push to the stack...
		// the event flow should be like this for the InputMethod:
		// onPushState -> So that the Input Method can store the data to the stack before calling "onDeactive"
		// onDeactive -> So that the Input Method can destroy any object that created (e.g. ExtensionFactory)
		// checkout the Input Method
		var im = this.GetCurrentInputMethod();
		this.state.im_context = {};
		if(im) {
			im.onPushState(this.state.im_context);
			im.onDeactive();
		}
		new_state = new GownFullState;
		this.state_stack.push(this.state);
		new_state.keyboard_observers = this.state.keyboard_observers.dup();
		this.state = new_state;
		this.UpdateUI();
	},
	PopState: function()
	{
		var old_state;

		old_state = this.state_stack.pop();
		if(old_state) {
			this.state = old_state;
			var im = this.GetCurrentInputMethod();
			if(im) {
				// after we pop from the stack
				// the event flow should be like this:
				// onActive -> Tell the Input Method that it is being selected
				// onPopState -> Tell the Input Method to restore from previous context
				im.onActive();
				im.onPopState(this.state.im_context);
			}
			delete this.state.im_context;
		}
		else this.state = new GownFullState;

		this.UpdateUI();
	},
	Execute: function(code,fun)
	{
		var f,inte;

		inte = this.state.interpreter;
		if(inte.running && !inte.breaking) return false;

//		inte.Reset();
		if(fun) inte.current_functional = fun;
		inte.AppendCode(code);
		Debug.trace(2,'GownFull: interpreter is running code="%s"...',code);
		f = inte.Run();
		if(!f) {
			Debug.trace(0,'GownFull: Interpreter is breaking...');
			return false;
		}
		Debug.trace(2,'GownFull: Interpreter terminated.');
		return true;
	},
	AddKeyboardObserver: function(fun,headp)
	{
		if(headp) this.state.keyboard_observers.push_head(fun);
		else this.state.keyboard_observers.push_tail(fun);
	},
	RemoveKeyboardObserver: function(fun)
	{
		var i;

		if(fun) {
			this.state.keyboard_pending_rm.push(fun);
			if(!this.state.keyboard_observers_inloop) this.RemoveKeyboardObserver();
		}
		else {
			if(this.state.keyboard_pending_rm.length == 0) return;

			var it = this.state.keyboard_observers.begin();
			var rmed;

			while(!it.nullp()) {
				rmed = false;
				for(i=0;i<this.state.keyboard_pending_rm.length;i++) {
					if(it.current() == this.state.keyboard_pending_rm[i]) {
						it.remove();
						rmed = true;
						break;
					}
				}
				if(!rmed) it.next();
			}
			this.state.keyboard_pending_rm = new Array();
		}
	},
	keyboard_handler: function(obj,type,key,modifiers)
	{
		var it = this.state.keyboard_observers.begin();
		var b = true;

		this.state.keyboard_observers_inloop = true;
		while(!it.nullp()) {
			b &= (it.current())(obj,type,key,modifiers,b);
			it.next();
		}
		this.state.keyboard_observers_inloop = false;
		this.RemoveKeyboardObserver();
		return b;
	},
/*
	keydown_handler: function(obj,key,modifiers)
	{
		var b,menu_called;

		Debug.trace(4,'GownFull: keydown_handler key=%d, modifiers=%b',key,modifiers);

		b = true;
		if(key == 8) {
			this.assoc.StopAssociativeWord();
			this.suggest.StopSuggestion();
		}

		menu_called = false;
		if(this.menu.available) {
			if(!this.menu.keydown_handler(obj,key,modifiers)) b = false;
			menu_called = true;
		}
		if(this.im_current > 0) {
			var im = this.im_array[this.im_current];
	
			b = im.keydown_handler(obj,key,modifiers);
		}
		if(b && !this.candwin.keydown_handler(obj,key,modifiers)) b = false;
		if(b && (34 <= key && key <= 40)) this.ResetPreeditIfSelect();

		if(!menu_called && !this.menu.keydown_handler(obj,key,modifiers)) b = false;

		Debug.trace(4,'GownFull: keydown_handler finally return %s',b);
		return b;
	},
	keyup_handler: function(obj,key,modifiers)
	{
		var b;

		Debug.trace(4,'GownFull: keyup_handler key=%d, modifiers=%b',key,modifiers);

		b = true;

		if(this.im_current > 0) {
			var im = this.im_array[this.im_current];
	
			b = im.keyup_handler(obj,key,modifiers);
		}
		if(key == 27) b = false; // Force return false for escape key
		Debug.trace(4,'GownFull: keyup_handler finally return %s',b);
		return b;
	},
	keypress_handler: function(obj,key,modifiers)
	{
		var b,menu_called;

		Debug.trace(4,'GownFull: keypress_handler key=%d, modifiers=%b',key,modifiers);

		b = true;
		menu_called = false;
		if(this.menu.available) {
			if(!this.menu.keypress_handler(obj,key,modifiers)) b = false;
			menu_called = true;
		}

		if(b && !this.candwin.keypress_handler(obj,key,modifiers)) b = false;
		if(b && this.im_current > 0) {
			var im = this.im_array[this.im_current];

			b = im.keypress_handler(obj,key,modifiers);
			this.assoc.StopAssociativeWord();
			this.suggest.StopSuggestion();
//			if(b) this.ClearSelectionPreedit();
		}
	
		if(!menu_called && !this.menu.keypress_handler(obj,key,modifiers)) b = false;
		// I want menu cannot be filtered. (no b checking)
		if(key == 27) b = false; // Force return false for escape key
		Debug.trace(4,'GownFull: keypress_handler finally return %s',b);
		return b;
	},
*/
	onfocus_handler: function(obj)
	{
		this.state.last_focus_obj = obj;
		if(obj) {
			Debug.trace(2,'GownFull: Set Last Focus Object=%s (%s)',obj,obj.nodeName);
		}
		else {
			Debug.trace(2,'GownFull: Set Last Focus Object=%s',obj);
		}
	},
	lostsel_handler: function(obj)
	{
		this.ResetPreeditIfSelect();
	},
	GetCurrentInputMethod: function()
	{
	// { displayname: null, modname: null, code }
		if(this.state.im_current > 0) {
			if(this.im_array[this.state.im_current].type == GownFull.LOCAL) {
				var im = this.im_array[this.state.im_current];
				im = this.modman.GetModule(im.modname);
				return im ? im : null;
			}
			else return null;
		}
		else return null;
	},
	GetCurrentInputMethodIndex: function()
	{
		if(this.state.im_current > 0) {
			if(this.im_array[this.state.im_current].type == GownFull.LOCAL) return this.state.im_current;
			else return 0;
		}
		else return 0;
	},
	LookupInputMethodByName: function(name)
	{
		var i,im;

		for(i=0;i<this.im_array.length;i++) {
			im = this.im_array[i];

			if(im.type == GownFull.DISABLED) continue;
			else {
				if(im.displayname == name) return i;
			}
		}
		return -1; // Not found
	},
	GetInputMethodCount: function()
	{
		return this.im_array.length;
	},
	GetInputMethodType: function(index)
	{
		var type = this.im_array[index];

		if(!type) return 0;
		type = type.type;
		return type;
	},
	GetInputMethodName: function(i)
	{
		if(this.im_array[i]) {
			if(this.im_array[i].type == GownFull.DISABLED) return $GFS('Disabled');
			else return this.im_array[i].displayname;
		}
		else return 'Error';
	},
	RegisterInputMethod: function(modname,displayname)
	{
		var im,mod;

		im = {};
		// check if the module having modname exists...
		mod = this.modman.GetModule(modname);
		if(mod) { // SHOULD BE an InputMethod
			im.displayname = mod.IMName();
			im.type = GownFull.LOCAL;
			im.modname = modname;
		}
		else {
			im.displayname = displayname;
			im.type = GownFull.REMOTE;
			im.modname = modname;
		}
		var i = this.im_array.length;
		this.im_array[i] = im;
		Debug.trace(0,"GownFull: Register a %s Input Method %d=(%s,%s)",(im.type == GownFull.LOCAL) ? "Local" : "Remote",i+1,im.displayname,im.modname);
		return (i+1);
	},
	SelectInputMethod: function(index)
	{
		var im,imo,status,name;

		Debug.trace(2,"GownFull: Selecting input method id=%d",index);

		// disable the current input method first
		if(this.state.im_current > 0) {
			imo = this.GetCurrentInputMethod();
			imo.onDeactive();
			this.state.im_current = 0;
		}

		if(index == 0) {
			this.state.im_current = 0;
			this.ResetPreedit();
			this.candwin.ReleaseAll();
			this.mainwin.UpdateIMName($GFS('Disabled'));
			return true;
		}
		im = this.im_array[index];
		if(!im) {
			Debug.trace(0,"GownFull: Input Method with id=%d does not exist. Something wrong.",index);
			return false;
		}
		this.ResetPreedit();
		this.candwin.ReleaseAll();

		status = im.type;
		switch(status) {
		case GownFull.DISABLED:
			this.mainwin.UpdateIMName($GFS('Disabled'));
			return true;
			break;
		case GownFull.LOCAL:
			this.state.im_current = index;
			if(this.state.im_current > 0) {
				imo = this.GetCurrentInputMethod();
				imo.onActive();
				im.displayname = imo.IMName();
				this.menu.UpdateIMNode(imo);
			}
			this.mainwin.UpdateIMName(im.displayname);
			Debug.trace(0,"GownFull: Input Method with id=%d, displayname=%s, modname=%s selected.",index,im.displayname,im.modname);
			return true;
			break;
		case GownFull.REMOTE:
			this.modman.Require(im.modname,
				(function(x,index) { return function(imo) { x.onCompleteDownloadIM(imo,index); } })(this,index)
			);
/*			var obj;

			obj = this.im_array[index];
			if(obj.url) this.DownloadRemoteInputMethod(obj.url,obj.objname,index);
			else this.DownloadInputMethod(obj.objname,index);
			name = obj.name;
			this.mainwin.UpdateIMName(name);
*/
			return true;
		default:
			Debug.trace(0,"GownFull: Input Method with id=%d has invalid status %d",index,status);
			return false;
		}
	},
	onCompleteDownloadIM: function(imo,index)
	{
		var im = this.im_array[index];
			
		if(imo) {
			im.type = GownFull.LOCAL;
			Debug.trace(0,"GownFull: Input Method \"%s\" downloaded successfully. (id=%d)",imo.IMName(),index);
			this.SelectInputMethod(index);
		}
		else {
			this.SetPreeditText(im.objname+' does not exist.');
		}
	},
	// implement the old interface API
	RegisterDownload: function(objname,name,url)
	{
		// objname is now become module name
		// name is the display name
		// url is url

		this.modman.RegisterDownload(objname,url,true);
		this.RegisterInputMethod(objname,name);
	},
	ToggleIME: function(show)
	{
		if(show && !this.state.available) {
			this.PopState();
//			this.SelectInputMethod(0);
			this.mainwin.ShowWindow(true);
			Debug.trace(1,"GownFull: ToggleIME on");
		}
		else if(!show && this.state.available) {
			this.PushState();
			this.state.available = false;
//			this.SelectInputMethod(0);
			this.mainwin.ShowWindow(false);
			this.candwin.ShowWindow(false);
			Debug.trace(1,"GownFull: ToggleIME off");
		}
	},
	GetPreedit: function() { return this.preedit; },
	ResetPreedit: function()
	{
		this.mainwin.ResetPreedit();
		this.preedit = '';
		this.display_preedit = '';
		this.ClearSelectionPreedit();
	},
	ResetPreeditIfSelect: function()
	{
		if(this.ClearSelectionPreedit()) {
			this.mainwin.ResetPreedit();
			this.preedit = '';
			this.display_preedit = '';
			var im = this.GetCurrentInputMethod();
			if(im) im.onSelectionModeResetPreedit();
		}
	},
	GetPreeditLength: function()
	{
		return this.preedit.length;
	},
	SetPreeditText: function(str)
	{
		this.mainwin.SetPreeditText(str);
		this.preedit = '';
		this.display_preedit = '';
		this.ClearSelectionPreedit();
	},
	// c - number of characters delete in preedit
	// dc - number of characters delete in display preedit
	DeletePreedit: function(c,dc,o)
	{
		var buf;

		if(dc == null) dc = 1;
		if(c == null) c = 1;

		this.mainwin.DeletePreedit(dc);
		this.preedit = this.preedit.substr(0,this.preedit.length - c);
		this.display_preedit = this.display_preedit.substr(0,this.display_preedit.length - c);
		if(this.display_preedit.length > 0) {
			var im = this.GetCurrentInputMethod();
			if(im && im.IsSelectionPreedit()) {
				if(!o) o = this.state.last_focus_obj;
				adapter = GownFullObjectAdapter.GetAdapter(o);
				if(adapter) {
					var s = adapter.GetSelectionStart();
					var e = adapter.GetSelectionEnd();
					adapter.InsertAtCursor(this.display_preedit);
					adapter.SetSelection(s,s+this.display_preedit.length);
					this.sel_preedit_adapter = adapter;
				}
			}
		}
		else this.ClearSelectionPreedit();
	},
	AppendPreedit: function(c,dc,o)
	{
		this.mainwin.AppendPreedit(dc);
		this.preedit += c;
		this.display_preedit += dc;
		var im = this.GetCurrentInputMethod();
		if(im && im.IsSelectionPreedit()) {
			if(!o) o = this.state.last_focus_obj;
			adapter = GownFullObjectAdapter.GetAdapter(o);
			if(adapter) {
				var s = adapter.GetSelectionStart();
				var e = adapter.GetSelectionEnd();
				adapter.InsertAtCursor(this.display_preedit);
				adapter.SetSelection(s,s+this.display_preedit.length);
				this.sel_preedit_adapter = adapter;
			}
		}
	},
	InvalidatePreedit: function()
	{
		this.mainwin.SetPreeditInvalid();
		this.preedit = '';
		this.ClearSelectionPreedit();
		this.display_preedit = '';
	},
	SetPreedit: function(s,ds,o)
	{
		this.mainwin.SetPreedit(ds);
		this.preedit = s;
		this.display_preedit = ds;
		var im = this.GetCurrentInputMethod();
		if(im && im.IsSelectionPreedit()) {
			if(!o) o = this.state.last_focus_obj;
			adapter = GownFullObjectAdapter.GetAdapter(o);
			if(adapter) {
				var s = adapter.GetSelectionStart();
				var e = adapter.GetSelectionEnd();
				adapter.InsertAtCursor(this.display_preedit);
				adapter.SetSelection(s,s+this.display_preedit.length);
				this.sel_preedit_adapter = adapter;
			}
		}
	},
	ClearSelectionPreedit: function()
	{
		if(this.sel_preedit_adapter) {
			this.sel_preedit_adapter.DeleteSelection();
			this.sel_preedit_adapter = null;
			return true;
		}
		else return false;
	},
	StartModifyOutputCode: function(obj)
	{
		var pmb;
		var pfc;
		var adapter;
		var x = this;

		// Get current postfix code
		pfc = this.state.postfix_code;
		// Undo the last send string...
		if(obj) {
			adapter = GownFullObjectAdapter.GetAdapter(obj);
			adapter.DisableUndo(true);
			adapter.Undo();
			adapter.DisableUndo();
			this.state.last_focus_obj = obj;
		}
		// get a new mini buffer
		pmb = this.miniwin_man.RequestMiniBuffer();
		// store the instance of pmb to current state
		this.state.modifycode_minibuf = pmb;
		// Store current GownFull state
		this.PushState();

		this.state.disable_modifycode = true;

		pmb.Start(obj);
		pmb.SetTitle($GFS('Modify Output Code'));
		pmb.SetInstruction($GFS('You can modify the output code here. Press Enter when your are finish.'));
		pmb.SetBuffer(pfc);

//		pmb.SetKeyMap(27,0,function(_this) { x.EndModifyOutputCode(); }); // End key code
		pmb.SetKeyMap(13,0,function(_this) { x.EndModifyOutputCode(_this,_this.GetBuffer()); }); // complete key code
		pmb.SetonBlurHandler(function(_this) { x.EndModifyOutputCode(_this); });

		adapter = pmb.GetAdapter();
		adapter.SetSelection(1,pfc.length);
	},
	EndModifyOutputCode: function(pmb,code)
	{
		var obj,adapter;

		pmb.End();

		if(code) {
			this.PopState();
			// Get the last focus object of the pushed state and obtain the adapter
			obj = this.state.last_focus_obj;
			if(obj) {
				adapter = GownFullObjectAdapter.GetAdapter(obj);
				adapter.InvalidateStack(GownFullObjectAdapter.REDO);
			}
			// release the minibuffer object
			this.miniwin_man.ReleaseMiniBuffer(this.state.modifycode_minibuf);
			this.state.modifycode_minibuf = null;
			// Evaluate the new code...
			this.Execute(code);
		}
		else {
			// pop the state...
			this.PopState();
			// Get the last focus object of the pushed state and obtain the adapter
			obj = this.state.last_focus_obj;
			if(obj) {
				adapter = GownFullObjectAdapter.GetAdapter(obj);
				adapter.DisableUndo(true);
				adapter.Redo();
				adapter.DisableUndo();
			}
			// release the minibuffer object
			this.miniwin_man.ReleaseMiniBuffer(this.state.modifycode_minibuf);
			this.state.modifycode_minibuf = null;
		}
	},
	SendHTML: function(o,s,code)
	{
		if(!code) code = this.state.postfix_code;

		var data;

		data = {};
		data.target = o;
		data.value = s;
		data.htmlp = true;

		this.Execute(code,GFFunctionalInterpreter.CreateJSFunctional(data));
	},
	SendString: function(o,s,code)
	{
		if(!code) code = this.state.postfix_code;

		var data;

		data = {};
		data.target = o;
		data.value = s;
		data.htmlp = false;

		this.Execute(code,GFFunctionalInterpreter.CreateJSFunctional(data));
	},
	_SendHTML: function(o,s)
	{
		if(!o) {
			o = this.state.last_focus_obj;
			if(o.focus) o.focus();
		}
		if(!o) {
			Debug.trace(0,"GownFull (SendHTML): No last focus object or target object.");
			return false;
		}

		// don't modifer HTML...now
		var adapter = GownFullObjectAdapter.GetAdapter(o);

		if(adapter) {
			var pos;

			if(adapter.SendHTML(o,s)) {
			}
			else { // This object does not support HTML output, output as string
				// enable undo
				adapter.InvalidateStack();
				adapter.DisableUndo(true);
				adapter.SendString(o,s);
				adapter.DisableUndo();
			}
			return true;
		}
		else {
			Debug.trace(0,"GownFull (SendHTML): No Adapter for this object.");
			return false;
		}
		
	},
	_SendString: function(o,s)
	{
		var b;

		if(!o) {
			o = this.state.last_focus_obj;
			if(o.focus) o.focus();
		}
		if(!o) {
			Debug.trace(0,"GownFull (SendString): No last focus object or target object.");
			return false;
		}

		Debug.trace(1,"GownFull: Send string \"%s\"",s);

		var adapter = GownFullObjectAdapter.GetAdapter(o);

		if(adapter) {
			adapter.InvalidateStack();
			adapter.DisableUndo(true);
			b = adapter.SendString(o,s);
			adapter.DisableUndo();
			return b;
		}
		else {
			Debug.trace(0,"GownFull (SendString): No Adapter for this object (nodeName=%s).",o.nodeName);
			return false;
		}
	}
},
{
	instance: null,
	DISABLED: 1,
	LOCAL: 2,
	REMOTE: 3
});
